""" S3D.ElectricCraft.ChipSaveLoadDialog module """
import os
import pickle
import shutil
from collections import deque
from PyQt4.QtGui import QDialog, QTreeWidgetItem, QGridLayout, QLabel, QDialogButtonBox, QSpinBox, QMessageBox, QIcon, QPixmap, QLineEdit
from PyQt4.QtCore import SIGNAL
from S3D.ElectricCraft.UI.ChipSaveLoadDialog import Ui_ChipSaveLoadDialog
from S3D.ElectricCraft.ViewModel import ViewModel
from S3D.ElectricCraft import ViewModel as game
from S3D import range_2

SAVES_PATH = "saves"
SAVE_EXT   = ".sav"

CLOSED_FOLDER_ICON  = None
OPENED_FOLDER_ICON  = None
ELEMENT_ICON        = None

def initIcons():
    global CLOSED_FOLDER_ICON
    global OPENED_FOLDER_ICON
    global ELEMENT_ICON

    if CLOSED_FOLDER_ICON is not None:
        return

    CLOSED_FOLDER_ICON = QIcon()
    CLOSED_FOLDER_ICON.addPixmap(QPixmap(":/icons/icons/Blank-icon.png"))

    OPENED_FOLDER_ICON = QIcon()
    OPENED_FOLDER_ICON.addPixmap(QPixmap(":/icons/icons/folder-images-icon.png"))

    ELEMENT_ICON = QIcon()
    ELEMENT_ICON.addPixmap(QPixmap(":/icons/icons/Mimetypes-Blank-Document-icon.png"))

class ChipsGroup:
    def __init__(self, parent, name = None, path = None):
        self.name   = name
        self.path   = path
        self.parent = parent
        self.treeItem = None
        self.groups = {}
        self.chips  = []

class ChipSaveLoadDialog(QDialog):
    def __init__(self, parent, isSave, model = None, isMainBoard = False):
        super(ChipSaveLoadDialog, self).__init__(parent)
        self.ui = Ui_ChipSaveLoadDialog()
        self.ui.setupUi(self)

        self.info  = None
        assert model or not isSave
        self.model = model
        self.isMainBoard = isMainBoard

        self.isSave = isSave
        if isSave:
            self.ui.createNewBtn.setVisible(False)
            self.ui.selectBtn.setVisible(False)
        else:
            self.ui.saveBtn.setVisible(False)
            self.ui.nameLabel.setVisible(False)
            self.ui.nameEdit.setVisible(False)

        self.ui.tree.addAction(self.ui.actionNewCategory)
        self.ui.tree.addAction(self.ui.actionRename)
        self.ui.tree.addAction(self.ui.actionDelete)

        self.collectChips()

    def collectChips(self):
        root = ChipsGroup(None)

        initIcons()

        # scan for .sav files
        for path, dirs, files in os.walk(SAVES_PATH):
            groups = path[len(SAVES_PATH) + 1:].split(os.sep)

            cur = root
            for index, p in enumerate(groups):
                if len(p) == 0:
                    continue

                if p not in cur.groups:
                    if p.startswith('.'):
                        cur = None
                        break
                        
                    next = ChipsGroup(cur, p, os.sep.join(groups[:index + 1]))
                    cur.groups[p] = next
                    cur = next
                else:
                    cur = cur.groups[p]

            if cur is None:
                continue

            for filename in files:
                name, ext = os.path.splitext(filename)
                if ext.lower() != SAVE_EXT:
                    continue

                cur.chips.append((os.path.join(path, filename), name))

        # fill tree
        queue = deque([root])
        while len(queue) > 0:
            cur = queue.popleft()

            if cur.parent is None:
                treeParent = QTreeWidgetItem([self.tr("saves")])
                treeParent.group = SAVES_PATH
                treeParent.setIcon(0, OPENED_FOLDER_ICON)

                root.treeItem = treeParent
                self.ui.tree.addTopLevelItem(treeParent)
            else:
                treeParent = cur.treeItem
                treeParent.group = os.path.join(SAVES_PATH, cur.path)
                
            treeParent.setExpanded(True)

            # add groups
            for gr in cur.groups.values():
                item = QTreeWidgetItem([gr.name])
                item.setIcon(0, CLOSED_FOLDER_ICON)
                gr.treeItem = item

                treeParent.addChild(item)
                queue.append(gr)

            # add saves
            for path, p in cur.chips:
                item = QTreeWidgetItem([p])
                item.setIcon(0, ELEMENT_ICON)
                item.path = path
                item.group = treeParent.group

                treeParent.addChild(item)

    #
    # Callbacks
    #
    def onCurrentItemChanged(self):
        item = self.ui.tree.currentItem()
        if item is None or not hasattr(item, "path"):
            return

        _, name = os.path.split(item.path)
        name, _ = os.path.splitext(name)
        self.ui.nameEdit.setText(name)

    def onSave(self):
        if not self.isSave:
            self.onSelect()
            return

        name = self.ui.nameEdit.text()
        if len(name) == 0:
            return

        item = self.ui.tree.currentItem()
        if item is None or not hasattr(item, "group"):
            return

        # check for overwrite
        if hasattr(item, "path"):
            _, prevName = os.path.split(item.path)
            prevName, _ = os.path.splitext(prevName)
            if prevName == name:
                if QMessageBox.Yes != QMessageBox().warning(self, self.tr("Are you sure?"),
                    self.tr("Are you sure you want to overwrite chip '%s'") % name,
                    QMessageBox.Yes | QMessageBox.No):
                    return
                
        path = os.path.join(item.group, name + SAVE_EXT)
        if self.saveViewModel(path, self.model):
            self.accept()

    def onSelect(self):
        if self.isSave:
            self.onSave()
            return

        item = self.ui.tree.currentItem()
        if not hasattr(item, "path"):
            return

        model = self.loadViewModel(item.path)
        if model is None:
            QMessageBox().critical(self, self.tr("Load failed"), self.tr("Failed to load file '%s'") % item.path)
            return

        self.info = (model, model.maxInputs)
        self.accept()

    def onCreateNew(self):
        if not self.isMainBoard:
            inputs = self.askNumber(self, 8, 8, self.tr("New chip"), self.tr("Enter inputs count:"))
            if inputs is None:
                return
        else:
            from S3D.ElectricCraft import Editor
            inputs = Editor.CHIP_MAX_INPUTS

        self.info = (None, inputs)
        self.accept()

    def onNewCategory(self):
        item = self.ui.tree.currentItem()
        if item is None or not hasattr(item, "group"):
            return

        name = self.askString(self, "", self.tr("New category"), self.tr("Enter category name:"))
        if name is None:
            return

        path = os.path.join(item.group, name)
        if os.access(path, os.F_OK):
            return

        try:
            print("mkdir:", path)
            os.mkdir(path)
        except:
            QMessageBox().critical(self, self.tr("Error"), self.tr("Invalid category name: '%s'") % name)
            return

        next = QTreeWidgetItem([name])
        next.setIcon(0, CLOSED_FOLDER_ICON)
        next.group = path

        # check for scheme items
        if not hasattr(item, "path"):
            item.addChild(next)
            item.setExpanded(True)
        else:
            parent = item.parent()
            parent.addChild(next)
            parent.setExpanded(True)

        self.ui.tree.setCurrentItem(next)

    def onRename(self):
        pass

    def onDelete(self):
        item = self.ui.tree.currentItem()
        if item is None:
            return

        if hasattr(item, "path"):
            res = QMessageBox().warning(self, self.tr("Delete chip"),
                self.tr("Are you sure you want to delete this chip?"),
                QMessageBox.Yes | QMessageBox.No)

            if res != QMessageBox.Yes:
                return

            try:
                print("delete:", item.path)
                os.remove(item.path)
            except:
                QMessageBox().critical(self, self.tr("Failed"),
                    self.tr("Failed to delete file: '%s'") % item.path)
                return

            item.parent().removeChild(item)
            return

        if hasattr(item, "group"):
            res = QMessageBox().warning(self, self.tr("Delete chips group"),
                self.tr("Are you sure you want to delete this group and all containing chips?"),
                QMessageBox.Yes | QMessageBox.No)

            if res != QMessageBox.Yes:
                return

            try:
                print("delete:", item.group)
                shutil.rmtree(item.group)
            except:
                QMessageBox().critical(self, self.tr("Failed"),
                    self.tr("Failed to delete folder: '%s'") % item.group)
                return

            item.parent().removeChild(item)
            return

    @staticmethod
    def askString(parent, value, title, message):
        dlg = QDialog(parent)
        dlg.setWindowTitle(title)

        layout = QGridLayout()
        layout.addWidget(QLabel(message), 0, 0)

        inputs = QLineEdit()
        inputs.setText(value)
        layout.addWidget(inputs, 0, 1)

        btnBox = QDialogButtonBox()
        btnBox.addButton(QDialogButtonBox.Ok)
        btnBox.addButton(QDialogButtonBox.Cancel)
        layout.addWidget(btnBox, 1, 0, 1, 2)
        dlg.setLayout(layout)

        dlg.connect(btnBox, SIGNAL("accepted()"), dlg.accept)
        dlg.connect(btnBox, SIGNAL("rejected()"), dlg.reject)
        if dlg.exec_() != QDialog.Accepted:
            return None

        return inputs.text()

    @staticmethod
    def askNumber(parent, value, maxValue, title, message):
        dlg = QDialog(parent)
        dlg.setWindowTitle(title)

        layout = QGridLayout()
        layout.addWidget(QLabel(message), 0, 0)

        inputs = QSpinBox()
        inputs.setMinimum(1)
        inputs.setMaximum(maxValue)
        inputs.setValue(value)
        layout.addWidget(inputs, 0, 1)

        btnBox = QDialogButtonBox()
        btnBox.addButton(QDialogButtonBox.Ok)
        btnBox.addButton(QDialogButtonBox.Cancel)
        layout.addWidget(btnBox, 1, 0, 1, 2)
        dlg.setLayout(layout)

        dlg.connect(btnBox, SIGNAL("accepted()"), dlg.accept)
        dlg.connect(btnBox, SIGNAL("rejected()"), dlg.reject)
        if dlg.exec_() != QDialog.Accepted:
            return None

        return inputs.value()        

    #
    # Save/load methods
    #
    @staticmethod
    def loadViewModel(name):
        from S3D.ElectricCraft import Editor

        _, modelName = os.path.split(name)
        modelName, _ = os.path.splitext(modelName)

        try:
            f = open(name, "rb")
            data = pickle.load(f)
            f.close()

            if isinstance(data, Editor.EditorSaveData):
                res = ViewModel(modelName, 3, *Editor.CHIP_BOARD_SIZE)
                for param in res.__slots__:
                    if hasattr(data, param):
                        setattr(res, param, getattr(data, param))
                    else:
                        print("param not found: %s" % param)
                        setattr(res, param, None)
            else:
                res = data
                
        except IOError:
            return None

        # reset groups
        ChipSaveLoadDialog.resetModel(res)
        ChipSaveLoadDialog.validateChips(res)

        res.name = modelName

        return res

    @staticmethod
    def resetModel(model):
        if model is None:
            return

        for x, y in range_2(model.sizeX, model.sizeY):
            p = model.items[x][y]
            if p is None:
                continue

            p.group = None if p.type != game.VIEW_ITEM_BRIDGE else [None, None]

            # top-left item
            if p.type == game.VIEW_ITEM_CHIP_INPUT and p.index == 0:
                ChipSaveLoadDialog.resetModel(p.chip.model)

        model.groups = []

    @staticmethod
    def validateChips(model):
        if model is None:
            return

        processed = []

        if not hasattr(model, "name"):
            model.name = "untitiled"

        # patch inputs & outputs arrays
        if not hasattr(model, "inputs") or model.inputs is None:
            model.inputs = []
        if not hasattr(model, "outputs") or model.outputs is None:
            model.outputs = []

        for x, y in range_2(model.sizeX, model.sizeY):
            item = model.items[x][y]
            if item is None:
                continue

            if item.type == game.VIEW_ITEM_INPUT or item.type == game.VIEW_ITEM_OUTPUT:
                if not hasattr(item, "index"):
                    item.index = None
                if item.index is None:
                    model.items[x][y] = None
                    # update index
                    model.setItem(x, y, item)

            if item.type in game.CHIP_PARTS:
                chip = item.chip
                if chip is not None and chip not in processed:
                    processed.append(chip)

                    ChipSaveLoadDialog.validateChips(chip.model)

        if not hasattr(model, "maxInputs") or model.maxInputs is None:
            print("missing maxInputs")
            model.maxInputs = max(len(model.inputs), len(model.outputs), 3)

    @staticmethod
    def saveViewModel(name, model):
        print("save", name)
        maxInputs = model.maxInputs
        try:
            model.maxInputs = max(len(model.inputs), len(model.outputs), 1)
            f = open(name, "wb")
            pickle.dump(model, f)
            f.close()
            return True
        except IOError:
            print("failed to save '%s'" % name)
            return False
        finally:
            model.maxInputs = maxInputs